/**
 * HexCharset.java
 * Copyright (C) 2013 Aspid
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package aspid.artwork.util;

import java.nio.*;
import java.nio.charset.*;

/**
 * An engine that can transform a sequence of bytes into a sequence of Unicode
 * characters representing those bytes as a hexadecimal number.
 *
 * An algorithm of transformation is the following. An input sequence divides
 * into pieces or <i>groups</i> of two bytes, and each group considers as
 * a separate big-endian number. The decoder outputs those numbers in
 * hexadecimal view placing a <i>delimiter</i> (dash character) between them.
 *
 * Currently size of groups, byte order, delimiter character and hex alphabet
 * are hard-coded constants, but we are planning to make them customizable
 * in a future.
 *
 * @see HexCharset
 */
class HexCharsetDecoder
    extends CharsetDecoder
{
    /* These values are precalculated for two bytes per group and
     * a single-character delimiter, and must be updated in case of
     * customization. The principal formula is:
     *
     *      M = 2 * N + d * floor((N - 1) / g),
     *
     * where M denotes the number of characters,
     *       N denotes the number of bytes,
     *       d denotes delimiter length (in characters), and
     *       g denotes grouping parameter (bytes per group).
     *
     * The exact value for AVG_CHARS_PER_BYTE is (2 + d / g), because
     * M = (2 + d / g) * N + O(1) as N tends to inifinity.
     *
     * The estimation for MAX_CHARS_PER_BYTE is also (2 + d / g), because
     * M <= 2 * N + d * (N - 1) / g < (2 + d / g) * N, but you can choose
     * a slightly greater number. Please note that periodical fractions will
     * cause a nasty bug in StringCoding, therefore you have to assign
     * a greater value if the grouping parameter is not a power of two.
     */
    private static final float AVG_CHARS_PER_BYTE = 2.5f;
    private static final float MAX_CHARS_PER_BYTE = 2.5f;

    private int numBytesDecoded = 0;

    private char[] digits;
    private int    grouping;
    private char   delimiter;

    /**
     * Initializes a new decoder.
     *
     * @param charset The charset that created this decoder.
     */
    public HexCharsetDecoder( HexCharset charset )
    {
        super(charset, AVG_CHARS_PER_BYTE, MAX_CHARS_PER_BYTE);

        this.digits    = "0123456789ABCDEF".toCharArray();
        this.grouping  = 2;
        this.delimiter = '-';
    }

    /**
     * Decodes one or more bytes into one or more characters.
     *
     * This method encapsulates the basic decoding loop, decoding as many
     * bytes as possible until it either runs out of input, runs out of room
     * in the output buffer, or encounters a decoding error. This method is
     * invoked by the {@link #decode decode} method, which handles result
     * interpretation and error recovery.
     *
     * The buffers are read from, and written to, starting at their current
     * positions. At most {@link java.nio.Buffer#remaining in.remaining()} bytes
     * will be read, and at most {@link java.nio.Buffer#remaining out.remaining()}
     * characters will be written. The buffers' positions will be advanced to
     * reflect the bytes read and the characters written, but their marks and
     * limits will not be modified.
     *
     * This method returns a {@link java.nio.charset.CoderResult} object to describe its
     * reason for termination, in the same manner as the {@link #decode decode}
     * method. Most implementations of this method will handle decoding errors
     * by returning an appropriate result object for interpretation by the
     * {@link #decode decode} method. An optimized implementation may instead
     * examine the relevant error action and implement that action itself.
     *
     * An implementation of this method may perform arbitrary lookahead by
     * returning {@link java.nio.charset.CoderResult#UNDERFLOW} until it receives sufficient
     * input.
     *
     * @param src The input byte buffer
     * @param dst The output character buffer
     * @return A coder-result object describing the reason for termination
     */
    protected CoderResult decodeLoop( ByteBuffer src, CharBuffer dst )
    {
        while (src.hasRemaining()) {
            /* Init transaction */
            src.mark();
            dst.mark();

            try {
                /* Transaction body */
                byte b = src.get();
                if (numBytesDecoded % grouping == 0 && numBytesDecoded != 0)
                    dst.put(delimiter);
                dst.put(getHighDigit(b));
                dst.put(getLowDigit(b));
            }
            catch (BufferOverflowException ex) {
                /* Rollback transaction */
                src.reset();
                dst.reset();
                return CoderResult.OVERFLOW;
            }

            /* Commit transaction*/
            ++numBytesDecoded;
        }

        return CoderResult.UNDERFLOW;
    }

    /**
     * Computes a character denoting a high-order half of a specified byte.
     *
     * @return Character <it>X</it> for a byte <tt>0xXY</tt>
     */
    private char getHighDigit( byte b )
    {
        return digits[(b >> 4) & 0x0F];
    }

    /**
     * Computes a character denoting a low-order half of a specified byte.
     *
     * @return Character <it>Y</it> for a byte <tt>0xXY</tt>
     */
    private char getLowDigit( byte b )
    {
        return digits[b & 0x0F];
    }
}

public class HexCharset
    extends Charset
{
    private static final HexCharset INSTANCE = new HexCharset();

    /**
     * Initialize a new charset.
     *
     * We do not recommend to invoke the constructor directly, because all
     * instances of the <tt>HexCharset</tt> class are completely equivalent.
     * Please use <tt>getInstance()</tt> method instead.
     */
    public HexCharset()
    {
        super("X-HEX16", null);
    }

    /**
     * Retrieve an instance of the charset.
     *
     * This method provides the only proper way to retrieve an instance of
     * this charset. Although using constructor is not illegal, it is not
     * recommended for that.
     *
     * @return Instance of the <tt>HexCharset</tt> class
     */
    public static HexCharset getInstance()
    {
        return INSTANCE;
    }

    /**
     * Tells whether or not this charset contains the given charset.
     *
     * A charset <i>C</i> is said to <i>contain</i> a charset <i>D</i> if,
     * and only if, every character representable in <i>D</i> is also
     * representable in <i>C</i>. If this relationship holds then it is
     * guaranteed that every string that can be encoded in <i>D</i> can also be
     * encoded in <i>C</i> without performing any replacements.
     *
     * That <i>C</i> contains <i>D</i> does not imply that each character
     * representable in <i>C</i> by a particular byte sequence is represented
     * in <i>D</i> by the same byte sequence, although sometimes this is the
     * case.
     *
     * Every charset contains itself.
     *
     * This method computes an approximation of the containment relation:
     * If it returns <tt>true</tt> then the given charset is known to be
     * contained by this charset; if it returns <tt>false</tt>, however, then
     * it is not necessarily the case that the given charset is not contained
     * in this charset.
     *
     * @return <tt>true</tt> if the given charset is contained in this charset
     */
    public boolean contains( Charset other )
    {
        return this.equals(other);
    }

    /**
     * Constructs a new decoder for this charset.
     *
     * @return A new decoder for this charset
     */
    public CharsetDecoder newDecoder()
    {
        return new HexCharsetDecoder(this);
    }

    /**
     * Constructs a new encoder for this charset.
     *
     * @return A new encoder for this charset
     */
    public CharsetEncoder newEncoder()
    {
//        return new HexCharsetEncoder(this);
        throw new UnsupportedOperationException("HexCharsetEncoder is not implemented yet");
    }
}
